﻿using System;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;


namespace LiveMail
{
    /// <summary>
    /// live mail base class.
    /// </summary>
    public abstract class TestMailBase : IDisposable
    {
        public const string DefaultOutlookUrl = "pod51009.outlook.com";
        public const int PopPort = 995;
        public const int InteractiveMailPort = 993;
        public const int SmtpPort = 587;

        private string connectionUrl;
        private int connectionPort;
        private bool connectionSsl;

        /// <summary>
        /// ctor method.
        /// </summary>
        protected TestMailBase(string host, int port, bool useSsl)
        {
            connectionUrl = host;
            connectionPort = port;
            connectionSsl = useSsl;
        }

        /// <summary>
        /// on server message received.
        /// </summary>
        public event EventHandler OnServerMessage;

        private const int DefaultTimeout = 30000;
        private const string NewLine = "\r\n";
        private Stream stream;
        private TcpClient clientSocket;

        /// <summary>
        /// connect to server.
        /// </summary>
        public virtual void Connect()
        {
            if (clientSocket != null && clientSocket.Connected)
            {
                clientSocket.Close();
            }
            clientSocket = new TcpClient();
            clientSocket.ReceiveTimeout = DefaultTimeout;
            clientSocket.SendTimeout = DefaultTimeout;

            try
            {
                clientSocket.Connect(connectionUrl, connectionPort);

                if (connectionSsl)
                {
                    SslStream ssl = new SslStream(clientSocket.GetStream(), false);
                    ssl.AuthenticateAsClient(connectionUrl);
                    stream = ssl;
                }
                else
                {
                    clientSocket.GetStream();
                }
            }
            catch
            {
                clientSocket.Close();
                throw;
            }
        }

        /// <summary>
        /// disconnect from server.
        /// </summary>
        public void Disconnect()
        {
            if (stream != null)
            {
                stream.Close();
                stream = null;
            }
            if (clientSocket != null && clientSocket.Connected)
            {
                clientSocket.Close();
                clientSocket = null;
            }

        }

        /// <summary>
        /// user login.
        /// </summary>
        public abstract void AuthenticateUser(string user, string pass);
        /// <summary>
        /// get total mail count.
        /// </summary>
        public abstract int ReadMailCount();
        /// <summary>
        /// get mail header content by header text.
        /// </summary>
        /// <param name="id">mail id.</param>
        public abstract NameValueCollection GetMail(int id);

        /// <summary>
        /// get mail method.
        /// </summary>
        /// <param name="command">command text sent to server.</param>
        protected NameValueCollection InternalGetMail(string command)
        {
            var collection = new NameValueCollection();
            Utilities.Retry(
                  () =>
                  {
                      string text = string.Empty;
                      text = SendCommand(command);

                      if (IsValidResponse(text))
                      {
                          text = ReadServerResponseText();
                          collection = ParseHeader(text);
                      }
                      return collection.Count < 1;
                  },
                  5);

            return collection;
        }

        /// <summary>
        /// verify resonse is ok of command. 
        /// </summary>
        protected static bool IsValidResponse(string res)
        {
            if (string.IsNullOrEmpty(res))
            {
                return false;
            }
            return res.StartsWith("+OK", StringComparison.CurrentCulture) || res.Contains("$ OK");
        }

        /// <summary>
        /// if command status is ok. read following content.
        /// </summary>
        protected string ReadServerResponseText()
        {
            int bufferSize = 128;
            using (MemoryStream arrayBuilder = new MemoryStream())
            {
                int bytes = -1;
                do
                {
                    byte[] buffer = new byte[bufferSize];
                    bytes = stream.Read(buffer, 0, buffer.Length);
                    arrayBuilder.Write(buffer, 0, buffer.Length);

                    if (bytes < bufferSize)
                    {
                        break;
                    }
                }
                while (bytes != 0);

                string res = Encoding.ASCII.GetString(arrayBuilder.ToArray());
                if (OnServerMessage != null)
                {
                    OnServerMessage(res, null);
                }
                return res;
            }


        }

        /// <summary>
        /// send command to server.
        /// </summary>
        /// <param name="cmd">command text.</param>
        protected string SendCommand(string cmd)
        {
            Console.WriteLine(cmd);
            byte[] bytes = Encoding.ASCII.GetBytes(cmd + NewLine);
            stream.Write(bytes, 0, bytes.Length);
            stream.Flush();
            string lastResponse = ReadServerResponseText();
            Console.WriteLine(lastResponse);
            if (IsValidResponse(lastResponse))
            {
                System.Diagnostics.Debug.WriteLine(cmd + NewLine + lastResponse);
            }
            return lastResponse;
        }

        /// <summary>
        /// get number of mails from command response.
        /// </summary>
        /// <param name="text">raw text.</param>
        protected static int ParseMailCountText(string text)
        {
            int num = 0;
            var matchText = Regex.Match(text, @"\d+").Value;
            if (int.TryParse(matchText, NumberStyles.Any, CultureInfo.CurrentCulture, out num))
            {
                return num;
            }
            return 0;
        }

        /// <summary>
        /// parse header text into name value collection. name will be in upper case.
        /// </summary>
        /// <param name="text">text from server.</param>
        protected static NameValueCollection ParseHeader(string text)
        {
            var collection = new NameValueCollection();
            using (StringReader sr = new StringReader(text))
            {
                while (sr.Peek() > 0)
                {
                    string line = sr.ReadLine().Trim();
                    int pos = line.IndexOf(':');
                    if (pos > 0)
                    {
                        string patten = @"\=\?(?<Encoding>.*?)\?(?<Base>\D+?)\?(?<Content>.*?)\?\=";
                        var m = Regex.Match(line, patten);
                        if (m.Success)
                        {
                            string codeName = m.Groups["Encoding"].Value;
                            string baseCode = m.Groups["Base"].Value;
                            string content = m.Groups["Content"].Value;
                            if (baseCode.ToUpperInvariant() == "B")
                            {
                                //support base64 only, skip if it's unknown
                                string tmp = Encoding.GetEncoding(codeName).GetString(Convert.FromBase64String(content));
                                line = Regex.Replace(line, patten, tmp);
                            }
                        }

                        string name = line.Substring(0, pos).Trim().ToUpperInvariant();
                        string value = line.Substring(pos + 1).Trim();
                        collection.Add(name, value);
                    }
                }
            }
            return collection;
        }

        /// <summary>
        /// dispose method.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (disposing)
            {
                Disconnect();
            }
        }

    }

    internal static class Utilities
    {
        [CLSCompliantAttribute(false)]
        public static void Retry(Func<bool> action, int totalNumberOfRetries)
        {
            int tryCount = 0;
            while (tryCount < totalNumberOfRetries)
            {
                try
                {
                    bool retry = action();
                    if (!retry)
                    {
                        break;
                    }
                }
                catch (Exception e)
                {
                    if (tryCount >= totalNumberOfRetries - 1)
                    {
                        throw new InvalidOperationException(e.Message, e);
                    }
                    System.Threading.Thread.Sleep(500);
                }
                ++tryCount;
            }
        }
    }
}
